﻿
using S7Cat.Models;
using S7Cat.Tools;
using System.Buffers.Binary;
using System.Runtime.CompilerServices;

namespace S7Cat.Protocol
{
    internal static class ConnectionRequest
    {
        private const byte SzlFunctionGroupTimers = 0x07;
        private const byte SzlSubFunctionReadClock = 0x01;
        private const byte SzlSubFunctionWriteClock = 0x02;
        private const byte TransportSizeOctetString = 0x09;
        private const int PduErrOffset = 20;
        private const int UserDataResultOffset = PduErrOffset + 2;
        private const int DateTimeLength = 10;

        public static byte[] GetCOTPConnectionRequest(TsapPair tsapPair)
        {
            return [
                3, 0, 0, 22, //TPKT
                17,     //COTP Header Length
                224,    //Connect Request
                0, 0,   //Destination Reference
                0, 46,  //Source Reference
                0,      //Flags
                193,    //Parameter Code (src-tasp)
                2,      //Parameter Length
                tsapPair.Local.FirstByte, tsapPair.Local.SecondByte,   //Source TASP
                194,    //Parameter Code (dst-tasp)
                2,      //Parameter Length
                tsapPair.Remote.FirstByte, tsapPair.Remote.SecondByte,   //Destination TASP
                192,    //Parameter Code (tpdu-size)
                1,      //Parameter Length
                10      //TPDU Size (2^10 = 1024)
             ];
        }

        public static byte[] GetS7ConnectionSetup()
        {
            return [
                3, 0, 0, 25, 2, 240, 128, 50, 1, 0, 0, 255, 255, 0, 8, 0, 0, 240, 0, 0, 3, 0, 3, 3, 192 // Use 960 PDU size
            ];
        }

        public static int SplitReadRequest(ReadOnlySpan<DataItemAddress> addresses, Span<Range> destination, int maxPDUSize)
        {
            int count = 0;
            var maxReadCount = (maxPDUSize - 19) / 12;
            int responseSize = 14;

            int start = 0, len = 0;

            for (int i = 0; i < addresses.Length; i++)
            {
                responseSize += addresses[i].ByteCount + 4;
                if (len >= maxReadCount || responseSize > maxPDUSize)
                {
                    i--;
                    destination[count++] = new Range(start, i);

                    responseSize = 14;
                    start = i;
                    len = 0;
                    continue;
                }

                len++;
            }

            if (len > 0)
            {
                destination[count++] = new Range(start, addresses.Length);
            }

            return count;
        }

        public static ArrayPoolBufferWriter<byte> BuildSplitReadRequest(ReadOnlySpan<DataItemAddress> addresses)
        {
            int packageSize = addresses.Length * 12 + 19;
            var buffer = new ArrayPoolBufferWriter<byte>(packageSize);
            PackageBuilder.WriteReadHeader(buffer, addresses.Length);
            for (int i = 0; i < addresses.Length; i++)
            {
                PackageBuilder.AddReadDataItemAddress(buffer, addresses[i]);
            }
            return buffer;
        }

        public static ArrayPoolBufferWriter<byte> BuildSplitReadRequest(DataItemAddress address)
        {
            int packageSize = 12 + 19;
            var buffer = new ArrayPoolBufferWriter<byte>(packageSize);
            PackageBuilder.WriteReadHeader(buffer, 1);
            PackageBuilder.AddReadDataItemAddress(buffer, address);
            return buffer;
        }

        public static int SplitWriteRequest(ReadOnlySpan<DataItemAddress> addresses, Span<Range> destination, int maxPDUSize)
        {
            int count = 0;
            var maxReadCount = (maxPDUSize - 12) / 18;
            int responseSize = 12;

            int start = 0, len = 0;

            for (int i = 0; i < addresses.Length; i++)
            {
                responseSize += addresses[i].ByteCount + 16;
                if (len >= maxReadCount || responseSize > maxPDUSize)
                {
                    i--;
                    destination[count++] = new Range(start, i);

                    responseSize = 12;
                    start = i;
                    len = 0;
                    continue;
                }

                if (i != addresses.Length - 1)
                {
                    len++;
                }
            }

            if (len > 0)
            {
                destination[count++] = new Range(start, addresses.Length);
            }

            return count;
        }

        public static ArrayPoolBufferWriter<byte> BuildSplitWriteRequest(ReadOnlySpan<DataItemAddress> addresses, Span<byte> bytes)
        {
            var buffer = new ArrayPoolBufferWriter<byte>(addresses.Length * 18 + 12);
            //Header Template
            buffer.Write(
                [
                    0x03, 0x00, 0x00, 0x00, // TPKT
                    0x02, 0xf0, 0x80, // ISO DT
                    0x32, // S7 protocol ID
                    0x01, // JobRequest
                    0x00, 0x00, // Reserved
                    0x05, 0x00, // PDU reference
                    0x00, 0x0e, // Parameters length
                    0x00, 0x00, // Data length
                    0x05, // Function: Write var
                    0x00, // Number of items to write
                ]
            );
            var span = buffer.Span;
            span[18] = (byte)addresses.Length;

            var paramSize = addresses.Length * 12;
            Unsafe.WriteUnaligned(ref span[13], BinaryPrimitives.ReverseEndianness((ushort)(2 + paramSize)));

            using var data = new ArrayPoolBufferWriter<byte>(addresses.Length * 4);

            var offset = 0;
            Span<byte> parameterTemplate = stackalloc byte[12];
            parameterTemplate[0] = 0x12;
            parameterTemplate[1] = 0x0a;
            parameterTemplate[2] = 0x10;
            for (var i = 0; i < addresses.Length; i++)
            {
                parameterTemplate[3] = addresses[i].IsBitValue ? (byte)1 : (byte)2;
                Unsafe.WriteUnaligned(ref parameterTemplate[4], BinaryPrimitives.ReverseEndianness(addresses[i].ByteCount));
                Unsafe.WriteUnaligned(ref parameterTemplate[6], BinaryPrimitives.ReverseEndianness(addresses[i].DB));
                parameterTemplate[8] = (byte)addresses[i].DataType;

                data.Write(0x00);
                //bool
                if (addresses[i].IsBitValue)
                {
                    var start = addresses[i].StartAdr * 8 + addresses[i].BitAdr;
                    parameterTemplate[11] = (byte)start;
                    start >>= 8;
                    parameterTemplate[10] = (byte)start;
                    start >>= 8;
                    parameterTemplate[9] = (byte)start;

                    data.Write(0x03);

                    data.WriteBigEndian((ushort)1);

                    data.Write(bytes.Slice(offset, addresses[i].ByteCount));

                    if (i != addresses.Length - 1)
                    {
                        data.Write(0x00);
                    }
                }
                else
                {
                    var start = addresses[i].StartAdr * 8;
                    parameterTemplate[11] = (byte)start;
                    start >>= 8;
                    parameterTemplate[10] = (byte)start;
                    start >>= 8;
                    parameterTemplate[9] = (byte)start;

                    data.Write(0x04);

                    data.WriteBigEndian((ushort)(addresses[i].ByteCount << 3));

                    data.Write(bytes.Slice(offset, addresses[i].ByteCount));

                    if ((addresses[i].ByteCount & 0b1) == 1 && i != addresses.Length - 1)
                    {
                        data.Write(0x00);
                    }
                }

                buffer.Write(parameterTemplate);

                offset += addresses[i].ByteCount;
            }

            buffer.Write(data.WrittenSpan);

            span = buffer.Span;

            Unsafe.WriteUnaligned(ref span[2], BinaryPrimitives.ReverseEndianness((ushort)buffer.WrittenCount));
            Unsafe.WriteUnaligned(ref span[15], BinaryPrimitives.ReverseEndianness((ushort)(buffer.WrittenCount - 19 - 12 * addresses.Length)));

            return buffer;
        }

        public static ArrayPoolBufferWriter<byte> BuildSplitWriteRequest(DataItemAddress address, ReadOnlySpan<byte> bytes)
        {
            var buffer = new ArrayPoolBufferWriter<byte>(35 + bytes.Length);
            //Header Template
            buffer.Write(
                [
                    0x03, 0x00, 0x00, 0x00, // TPKT
                    0x02, 0xf0, 0x80, // ISO DT
                    0x32, // S7 protocol ID
                    0x01, // JobRequest
                    0x00, 0x00, // Reserved
                    0x05, 0x00, // PDU reference
                    0x00, 0x0e, // Parameters length
                    0x00, 0x00, // Data length
                    0x05, // Function: Write var
                    0x00, // Number of items to write
                ]
            );
            var span = buffer.Span;
            span[18] = 0x01;

            var paramSize = 12;
            Unsafe.WriteUnaligned(ref span[13], BinaryPrimitives.ReverseEndianness((ushort)(2 + paramSize)));

            using var data = new ArrayPoolBufferWriter<byte>(4);

            //Parameter Template
            //byte[] parameterTemplate = [
            //    0x12, // Spec
            //        0x0a, // Length of remaining bytes
            //        0x10, // Addressing mode
            //        0x02, // Transport size
            //        0x00, 0x00, // Number of elements
            //        0x00, 0x00, // DB number
            //        0x84, // Area type
            //        0x00, 0x00, 0x00 // Area offset
            //];
            Span<byte> parameterTemplate = stackalloc byte[12];
            parameterTemplate[0] = 0x12;
            parameterTemplate[1] = 0x0a;
            parameterTemplate[2] = 0x10;

            parameterTemplate[3] = address.IsBitValue ? (byte)1 : (byte)2;
            Unsafe.WriteUnaligned(ref parameterTemplate[4], BinaryPrimitives.ReverseEndianness(address.ByteCount));
            Unsafe.WriteUnaligned(ref parameterTemplate[6], BinaryPrimitives.ReverseEndianness(address.DB));
            parameterTemplate[8] = (byte)address.DataType;


            data.Write(0x00);
            //bool
            if (address.IsBitValue)
            {
                var start = address.StartAdr * 8 + address.BitAdr;
                parameterTemplate[11] = (byte)start;
                start >>= 8;
                parameterTemplate[10] = (byte)start;
                start >>= 8;
                parameterTemplate[9] = (byte)start;

                data.Write(0x03);

                data.WriteBigEndian((ushort)1);

                data.Write(bytes);
            }
            else
            {
                var start = address.StartAdr * 8;
                parameterTemplate[11] = (byte)start;
                start >>= 8;
                parameterTemplate[10] = (byte)start;
                start >>= 8;
                parameterTemplate[9] = (byte)start;

                data.Write(0x04);

                data.WriteBigEndian((ushort)(address.ByteCount << 3));

                data.Write(bytes);
            }

            buffer.Write(parameterTemplate);

            buffer.Write(data.WrittenSpan);

            span = buffer.Span;

            Unsafe.WriteUnaligned(ref span[2], BinaryPrimitives.ReverseEndianness((ushort)buffer.WrittenCount));
            Unsafe.WriteUnaligned(ref span[15], BinaryPrimitives.ReverseEndianness((ushort)(buffer.WrittenCount - 19 - 12)));

            return buffer;
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static void ThrowIfRequestSizeTooLong(int requestSize, int maxPDUSize)
        {
            if (requestSize > maxPDUSize)
            {
                throw new Exception("Request size is too long.");
            }
        }


        public static ArrayPoolBufferWriter<byte> BuildReadClockRequest()
        {
            var buffer = new ArrayPoolBufferWriter<byte>(29);
            PackageBuilder.WriteUserDataRequest(buffer, SzlFunctionGroupTimers, SzlSubFunctionReadClock, 4);
            buffer.Write([0x0a, 0x00, 0x00, 0x00]);

            return buffer;
        }

        public static ArrayPoolBufferWriter<byte> BuildWriteClockRequest(DateTime value)
        {
            var buffer = new ArrayPoolBufferWriter<byte>(39);
            PackageBuilder.WriteUserDataRequest(buffer, SzlFunctionGroupTimers, SzlSubFunctionReadClock, 14);
            // Start of DateTime value, DateTime.ToByteArray only serializes the final 8 bytes
            buffer.Write([0xff, TransportSizeOctetString, 0x00, DateTimeLength]);

            buffer.Write([0x00, 0x19]);

            using var dateTimeBuffer = PackageParser.DateTimeToBytes(value);

            buffer.Write(dateTimeBuffer.WrittenSpan);

            return buffer;
        }
    }
}
